package algorithm;

import dataset.Dataset;
import exception.KNoMeaningException;
import scicalc.Calculate;
import scicalc.Collection;

import java.util.*;


public class DAKMeans extends AbstractClusterAlgorithm {
    private int k = 3;
    private Linkage linkage = Linkage.AVERAGE_LINKAGE;
    private double epsilon = 0.11;
    private int minPts = 5;
    List<Integer> Gamma = new ArrayList<>();


    @Override
    public long fit(Dataset X) {
        long start = System.currentTimeMillis();
        List<Integer> first = outliers(X.dataset, this.epsilon, this.minPts);
        double[][] pointChoose = Calculate.choose(X.dataset, first);
        double[][] mu = setMu(pointChoose, this.k);
        while (true) {
            Map<Integer, List<Integer>> clusters = new HashMap<>();
            for (int i = 0; i < this.k; i++) {
                clusters.put(i, new ArrayList<>());
            }
            for (int i = 0; i < pointChoose.length; i++) {
                double[] distance = Calculate.sqrt(Calculate.sum(Calculate.pow(Calculate.minus(pointChoose[i], mu), 2), Calculate.AXIS_1));
                int lambda = Calculate.argmin(distance);
                clusters.get(lambda).add(i);
            }
            double[][] mu_ = new double[mu.length][mu[0].length];
            for (int i = 0; i < k; i++) {
                double[][] choose = Calculate.choose(pointChoose, clusters.get(i));
                mu_[i] = Calculate.mean(choose, Calculate.AXIS_0);
            }
            if (Calculate.sum(Calculate.pow(Calculate.minus(mu_, mu), 2)) < 1e-8) {
                labels = new int[pointChoose.length];
                for (int i = 0; i < k; i++) {
                    List<Integer> list = clusters.get(i);
                    for (Integer number : list)
                        labels[number] = i;
                }
                break;
            } else {
                mu = mu_;
            }
        }
        double[][] resultData = new double[X.shape[0]][X.shape[1]];
        int[] resultLabel = new int[X.shape[0]];
        for (int i = 0; i < pointChoose.length; i++) {
            resultData[i] = pointChoose[i];
            resultLabel[i] = labels[i];
        }
        for (int i = 0; i < Gamma.size(); i++) {
            resultData[i + pointChoose.length] = X.dataset[Gamma.get(i)];
            resultLabel[i + pointChoose.length] = -1;
        }
        result.shape[0] = X.shape[0];
        result.shape[1] = X.shape[1];
        result.dataset = resultData;
        result.label = resultLabel;
        return System.currentTimeMillis() - start;
    }

    private List<Integer> outliers(double[][] X, double epsilon, int minPts) {
        Map<Integer, ArrayList<Integer>> nEpsilon = new HashMap<>();
        List<Integer> Omega = new ArrayList<>();
        for (int j = 0; j < X.length; j++) {
            nEpsilon.put(j, new ArrayList<>());
            double[] dist = Calculate.sqrt(Calculate.sum(Calculate.pow(Calculate.minus(X, X[j]), 2), Calculate.AXIS_1));
            for (int i = 0; i < dist.length; i++) {
                if (dist[i] <= this.epsilon)
                    nEpsilon.get(j).add(i);
            }
            if (nEpsilon.get(j).size() >= this.minPts)
                Omega.add(j);
        }
        for (int i = 0; i < X.length; i++)
            Gamma.add(i);
        while (!Omega.isEmpty()) {
            Queue<Integer> Q = new LinkedList<>();
            int o = Omega.get(Calculate.randomInt(0, Omega.size()));
            Q.offer(o);
            Gamma = Collection.difference(Gamma, o);
            while (!Q.isEmpty()) {
                int q = Q.poll();
                if (nEpsilon.get(q).size() >= minPts) {
                    List<Integer> Delta = Collection.intersection(nEpsilon.get(q), Gamma);
                    for (Integer i : Delta)
                        Q.offer(i);
                    Gamma = Collection.difference(Gamma, Delta);
                }
                Omega = Collection.difference(Omega, q);
            }
        }
        List<Integer> res = new ArrayList<>();
        for (int i = 0; i < X.length; i++)
            res.add(i);
        return Collection.difference(res, Gamma);
    }

    private double[][] setMu(double[][] X, int k) {
        Map<Integer, List<Integer>> clusters = new HashMap<>();
        for (int i = 0; i < X.length; i++) {
            List<Integer> list = new ArrayList<>();
            list.add(i);
            clusters.put(i, list);
        }
        double[][] M = new double[X.length][X.length];
        for (int i = 0; i < X.length; i++) {
            M[i][i] = Double.MAX_VALUE;
            for (int j = i + 1; j < X.length; j++) {
                M[i][j] = linkage.distance(X, clusters.get(i), clusters.get(j));
                M[j][i] = M[i][j];
            }
        }
        int q = X.length;
        while (q > k) {
            int index = Calculate.argmin(M);
            int i_ = index / M.length;
            int j_ = index % M.length;
            clusters.put(i_, Collection.union(clusters.get(i_), clusters.get(j_)));
            for (int i = j_ + 1; i < q; i++)
                clusters.put(i - 1, clusters.get(i));
            clusters.remove(q - 1);
            M = Calculate.delete(M, j_, Calculate.AXIS_0);
            M = Calculate.delete(M, j_, Calculate.AXIS_1);
            q--;
            for (int j = 0; j < q; j++) {
                if (i_ != j) {
                    M[i_][j] = linkage.distance(X, clusters.get(i_), clusters.get(j));
                    M[j][i_] = M[i_][j];
                }
            }
        }
        double[][] mu = new double[clusters.size()][];
        for (int i = 0; i < clusters.size(); i++)
            mu[i] = Calculate.mean(Calculate.choose(X, clusters.get(i)), Calculate.AXIS_0);
        return mu;
    }

    public void setEpsilon(double epsilon) {
        this.epsilon = epsilon;
    }

    public void setMinPts(int minPts) {
        this.minPts = minPts;
    }

    public void setK(int k) {
        if (k < 2) {
            try {
                throw new KNoMeaningException(k);
            } catch (KNoMeaningException e) {
                e.printStackTrace();
            }
        } else this.k = k;
    }

    public void setLinkage(Linkage linkage) {
        this.linkage = linkage;
    }
}
